# Defines all of the vote weighting algorithms
#
# To create a new algorithm, inherit from WeightingAlgorithm and redefine
# all of its methods. Templates for any methods should be created in
# templates/CLASSNAME/.
#
# Movie.VOTING_ALGORITHM should reference one of these classes

from django.contrib.auth.models import User
from django.shortcuts import render_to_response

import collections
import movie.vote.models


class WeightingAlgorithm(object):
  """All algorithms should inherit from this class and redefine its methods."""

  def __str__(self):
    """One or two word name of algorithm.

    Returns:
      string of 1 or 2 words
    """
    raise NotImplemented

  def get_weight(self, user):
    """Get the vote weight for a given user.
  
    Args:
      user: User object

    Returns:
      float weight value
    """
    raise NotImplemented

  def user_stats(self, user):
    """Output a human-readable description of the weighting algorithm and
    possibly details about how a user's stats are calculated.

    Args:
      user: User object
    
    Returns:
      a view response, such as generated by render_to_response()
    """
    raise NotImplemented
      

class Egalitarian(WeightingAlgorithm):
  """Everyone gets a 1 vote."""

  def __str__(self):
    return "Egalitarian"

  def get_weight(self, user):
    return 1

  def user_stats(self, user):
    weight = self.get_weight(user)

    stats = {}
    stats['weight'] = weight
    stats['username'] = user.username

    classname = self.__class__.__name__
    return render_to_response('weighting/%s/user_stats.html' % (classname,), stats)


class Badness(WeightingAlgorithm):
  """Vote value is based on user's movie rating deviation from the average."""

  def __str__(self):
    return "Badness"

  def get_weight(self, user):
    movie_rating_sum = collections.defaultdict(int)
    movie_rating_count = collections.defaultdict(int)
    user_movie_rating = {}

    for rating in movie.vote.models.Rating.objects.all():
      if rating.user == user:
        user_movie_rating[rating.movie] = rating.value
        continue
      movie_rating_sum[rating.movie] += rating.value
      movie_rating_count[rating.movie] += 1

    deviation = 0.0
    intersected_ratings = 0
    for m, rating in user_movie_rating.iteritems():
      if m in movie_rating_count:
        average = movie_rating_sum[m]/(float(movie_rating_count[m]))
        deviation += abs(average - rating)
        intersected_ratings += 1
    if intersected_ratings == 0:
      value = 0.5
    else:
      average_deviation = deviation/intersected_ratings
      value = 2.5 - average_deviation
    return value * max(.1, RecentPercentage().get_weight(user))

  def user_stats(self, user):
    weight = self.get_weight(user)

    stats = {}
    stats['weight'] = weight
    stats['username'] = user.username

    classname = self.__class__.__name__
    return render_to_response('weighting/%s/user_stats.html' % (classname,), stats)


class RecentPercentage(WeightingAlgorithm):
  """Vote values determined by percentage of last 10 screenings attended.

  For example, if a user has attended 5 of the last 10 screenings, his value
  would be .5.
  """
  RECENT_LIMIT = 10
  
  def __str__(self):
    return "Recent Percentage"

  def get_weight(self, user):
    attended = 0
    for screening in movie.vote.models.Screening.objects.order_by('-date')[:self.RECENT_LIMIT]:
      if screening.attendees.filter(username=user.username).count():
        attended += 1
    return attended / float(self.RECENT_LIMIT)

  def user_stats(self, user):
    weight = self.get_weight(user)
    attended = int(weight * self.RECENT_LIMIT)
    missed = self.RECENT_LIMIT - attended

    stats = {}
    stats['missed'] = missed
    stats['attended'] = attended
    stats['weight'] = weight
    stats['username'] = user.username

    classname = self.__class__.__name__
    return render_to_response('weighting/%s/user_stats.html' % (classname,), stats)
  

class OldSchool(WeightingAlgorithm):
  RECENT_SCREENINGS = 5

  def __str__(self):
    return "Old School"

  def get_weight(self, user):
    """The formula is:
    
    If there has been no screenings, always return 1.

    attended = number of screenings attended
    missed = number of screenings missed
    attended_recent = number of past RECENT_SCREENINGS screenings that were attended
    missed_recent = number of past RECENT_SCREENINGS screenings that were missed

    all_time_score = max(-0.5, min(0.5, 0.05 * (attended - missed)))
    recent_score = 0.1 * (attended_recent - missed_recent)
    lock = attended >= 1 ? 0.5 : 0
    vote value = max(0, min(1, lock + all_time_score + recent_score))

    NOTE: If you change this algorithm, you should update views.user_stats to
    reflect the changes.
    """
    total_screenings = movie.vote.models.Screening.objects.count()
    if not total_screenings:
      # If no screenings, treat everyone's vote value as 1
      return 1

    # Figure out account inception date
    # Find out earliest screening they've attended
    # If earliest screening date is less than account inception date, use that
    inception_date = user.date_joined.date()
    attended = user.attended_screenings.count()
    if attended:
      oldest_screen_date = user.attended_screenings.order_by('date')[0].date
      if oldest_screen_date < inception_date:
        inception_date = oldest_screen_date

    screenings_inception = movie.vote.models.Screening.objects.filter(date__gte=inception_date)
    missed = screenings_inception.count() - attended
    attended_recent = 0
    missed_recent = 0
    for screening in screenings_inception.order_by('-date')[:self.RECENT_SCREENINGS]:
      if screening.attendees.filter(username=user.username).count():
        attended_recent += 1
      else:
        missed_recent += 1

    # restrict all_time_score : -0.5 <= all_time_score <= 0.5
    all_time_score = max(-0.5, min(0.5, 0.05 * (attended - missed)))
    recent_score = 0.1 * (attended_recent - missed_recent)
    lock = 0
    if attended >= 1:
      lock = 0.5
    # restrict weight : 0 <= weight <= 1
    return max(0, min(1, lock + all_time_score + recent_score))

  def user_stats(self, user):
    total_screenings = movie.vote.models.Screening.objects.count()
    inception_date = user.date_joined.date()
    attended = user.attended_screenings.count()
    if attended:
      oldest_screen_date = user.attended_screenings.order_by('date')[0].date
      if oldest_screen_date < inception_date:
        inception_date = oldest_screen_date

    screenings_inception = movie.vote.models.Screening.objects.filter(date__gte=inception_date)
    missed = screenings_inception.count() - attended
    attended_recent = 0
    missed_recent = 0
    for screening in screenings_inception.order_by('-date')[:self.RECENT_SCREENINGS]:
      if screening.attendees.filter(username=user.username).count():
        attended_recent += 1
      else:
        missed_recent += 1

    all_time_score = max(-0.5, min(0.5, 0.05 * (attended - missed)))
    recent_score = 0.1 * (attended_recent - missed_recent)
    lock = 0
    if attended >= 1:
      lock = 0.5
    weight = max(0, min(1, lock + all_time_score + recent_score))

    stats = {}
    stats['total_screenings'] = total_screenings
    stats['screenings_inception'] = screenings_inception.count()
    stats['attended'] = attended
    stats['missed'] = missed
    stats['attended_recent'] = attended_recent
    stats['missed_recent'] = missed_recent
    stats['all_time_score'] = all_time_score
    stats['recent_score'] = recent_score
    stats['lock'] = lock
    stats['weight'] = weight
    stats['username'] = user.username

    classname = self.__class__.__name__
    return render_to_response('weighting/%s/user_stats.html' % (classname,), stats)


class ReverseCowgirl(OldSchool):
  """The inverse of OldSchool."""

  def __str__(self):
    return "Reverse Cowgirl"

  def get_weight(self, user):
    return 1 - super(ReverseCowgirl, self).get_weight(user)


class YouSuck(OldSchool):
  """Penalize certain users."""
  LAME_USERS = ()

  def __str__(self):
    return "You Suck"

  def get_weight(self, user):
    if user.username in self.LAME_USERS:
      return 0
    return super(YouSuck, self).get_weight(user)

  def user_stats(self, user):
    classname = self.__class__.__name__
    if user.username in self.LAME_USERS:
      stats = { 'username': user.username }
      return render_to_response('weighting/%s/user_stats_lame.html' % (classname,), stats)
    else:
      return super(YouSuck, self).user_stats(user)
